Skip to content

Improve decoder throughput with buffered scalar reads - #299

Open
Saurabh Singh (saurabh500) wants to merge 3 commits into
dev/saurabh/native-resultset-futuresfrom
dev/saurabh/sync-buffered-reads
Open

Improve decoder throughput with buffered scalar reads#299
Saurabh Singh (saurabh500) wants to merge 3 commits into
dev/saurabh/native-resultset-futuresfrom
dev/saurabh/sync-buffered-reads

Conversation

@saurabh500

@saurabh500 Saurabh Singh (saurabh500) commented Aug 14, 2026

Copy link
Copy Markdown
Contributor

Description

Improves row-decoder throughput by synchronously consuming fixed-width scalars that are already buffered in NetworkTransport. A buffer miss falls back to the existing async TDS packet read/refill path and retries, so packet framing and error handling remain authoritative.

The optimized decoder reads cover byte, little-endian i16, u16, 24-bit u32, i32, u32, 40-bit u64, i64, f32, and f64. This is a standalone runtime optimization informed by measurements from #269. It does not revive that PR or retry decoder convergence.

Stack position

This draft is stacked directly on #291 (Remove ResultSet async_trait boxing) in native stack #287. Its incremental diff is the two buffered-read commits above #291; the current open chain is #286#291 → this PR.

Implementation

  • Adds explicit non-consuming-on-miss probes to both TdsPacketReader configurations, with safe None defaults for other readers.
  • Implements direct fixed-width probes in TdsReadBuffer and delegates them from NetworkTransport.
  • Uses one parameterized read_sync_first! macro at every production fixed-width scalar read in datatypes/decoder.rs.
  • Keeps read_tds_packet() as the only refill path for async misses.
  • Leaves big-endian packet/header reads and u64 reads outside the decoder unchanged.

Before and after

Before: every scalar enters the async path

sequenceDiagram
    participant D as Decoder
    participant A as async read_*()
    participant B as TdsReadBuffer
    participant P as Packet I/O

    D->>A: read_*().await
    Note over D,A: Construct and poll a future for every scalar
    A->>B: Enough bytes buffered?
    alt Buffered hit
        B-->>A: Yes
        A->>B: Consume N bytes
        A-->>D: Ready(value)
    else Buffer miss
        B-->>A: No
        A->>P: read_tds_packet().await
        P-->>A: Next framed payload
        A->>B: Retry and consume N bytes
        A-->>D: Ready(value)
    end
Loading

After: synchronous probe before async fallback

sequenceDiagram
    participant D as Decoder
    participant M as sync-first macro
    participant T as NetworkTransport
    participant B as TdsReadBuffer
    participant P as Packet I/O

    D->>M: read_sync_first!
    M->>T: try_read_*() [sync]
    T->>B: Probe complete N-byte scalar
    alt Buffered hit
        B-->>T: Some(value), consume N bytes
        T-->>M: Some(value)
        M-->>D: value
        Note over D,M: No async future constructed or polled
    else Buffer miss
        B-->>T: None, consume zero bytes
        T-->>M: None
        M->>T: read_*().await
        T->>P: read_tds_packet().await
        P-->>T: Next framed payload
        T->>B: Refill, retry, consume complete scalar
        T-->>M: Ok(value)
        M-->>D: value
    end
Loading

N is the scalar's fixed wire width (1, 2, 3, 4, 5, or 8 bytes). Packet framing, cancellation, encryption, and errors remain in the existing async fallback.

Correctness coverage

Targeted tests cover successful buffered reads, zero consumption when any supported scalar is incomplete, and every supported scalar split across real TDS packet boundaries through NetworkTransport. Existing NULL and length-marker handling remains unchanged.

Production-reader benchmarks

All benchmarks use concrete NetworkTransport, packetized in-memory input, 7,992-byte packet payloads, 30,000 rows per pass, 2 warmups, 9 measured passes per cell, and 8 paired rounds alternating baseline/candidate order. Negative deltas are faster.

Stacked result versus #291 (8c6ff8b2)

Workload Sink Paired median Range #291 median Stacked median
fixed scalars discard -38.16% -40.12% to -37.12% 70.23 ms 43.40 ms
fixed scalars materialize -25.23% -26.53% to -21.80% 104.24 ms 78.40 ms
INT/VARCHAR discard -14.70% -17.96% to -12.73% 58.55 ms 50.04 ms
INT/VARCHAR materialize -16.63% -17.86% to -14.91% 95.57 ms 79.60 ms
mixed discard -12.90% -16.72% to -10.83% 123.27 ms 107.37 ms
mixed materialize -11.06% -12.90% to -8.72% 163.29 ms 145.73 ms

The stack comparison used identical lockfiles and isolated source/target directories. The #291 and stacked test binaries had different SHA-256 hashes.

Initial change versus clean main (ac1023a1)

Workload Sink Paired median Range Main median Candidate median
INT/VARCHAR discard -6.06% -15.86% to +11.50% 62.25 ms 58.46 ms
INT/VARCHAR materialize -14.32% -21.77% to -11.27% 99.75 ms 85.89 ms
mixed discard -11.04% -22.62% to -1.52% 128.02 ms 112.58 ms
mixed materialize -10.49% -20.84% to +3.20% 172.04 ms 153.39 ms

Expanded fixed-width reads versus the first draft (47c2fb68)

The fixed_scalars workload has 64 columns: Int1, Int2, Int4, Int8, Flt4, Flt8, DateTime, and DateTim4, repeated eight times.

Workload Sink Paired median Range First draft median Expanded median
fixed scalars discard -18.14% -20.76% to -16.08% 95.55 ms 78.48 ms
fixed scalars materialize -9.62% -21.52% to +6.46% 155.71 ms 140.14 ms
INT/VARCHAR discard +0.06% -3.87% to +3.78% 93.63 ms 94.03 ms
INT/VARCHAR materialize +1.01% -10.87% to +13.10% 144.72 ms 145.27 ms
mixed discard -0.95% -4.79% to +2.29% 193.05 ms 189.67 ms
mixed materialize -1.02% -5.07% to +9.28% 264.07 ms 261.60 ms

The optimized pre-stack benchmark executable grew by about 192 KB (1.75%), so code-size impact remains a review consideration.

Validation

  • cargo bfmt
  • cargo bclippy
  • $env:RUSTFLAGS='--cfg fuzzing'; cargo check -p mssql-tds --lib
  • Rebased full mssql-tds library nextest suite with generated TLS fixtures: 1,764 passed
  • Stacked production-reader benchmark against Remove ResultSet async_trait boxing #291: all six paired medians improved
  • GitHub CodeQL, linked-issue, CLA, and coverage checks pass
  • Stacked main...HEAD coverage: 98% diff coverage and 91.5% overall coverage
  • The external ADO build policy skips dependent-branch PRs; the same buffered-read commits passed its full cross-platform matrix before restacking (build 167366)
  • cargo btest was attempted locally before stacking but the live SQL integration tests fail with Schannel SEC_E_WRONG_PRINCIPAL.

Risk

The fast path is limited to already-buffered decoder scalars on NetworkTransport. Other packet readers retain their existing async behavior through default None probes. Misses preserve partial bytes and reuse the existing refill path, including cancellation and encryption behavior. The broader set of inlined probes trades some code size for lower per-value async overhead.

Related Issues

Related to #247.

Stacked on #291.

Historical measurement context: #269.

Checklist

  • cargo bfmt passes
  • cargo bclippy passes
  • cargo btest passes locally (environment-blocked; pre-stack remote matrix passes)
  • New/changed functionality has tests
  • Public API changes are documented

Copilot AI left a comment

Copy link
Copy Markdown
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Pull request overview

Optimizes TDS row decoding by synchronously consuming buffered scalar values while retaining the existing asynchronous packet-refill fallback.

Changes:

  • Adds optional scalar probes to TdsPacketReader.
  • Implements buffered byte, u16, and i32 reads for NetworkTransport.
  • Routes decoder hot paths through sync-first reads and adds boundary tests.

Reviewed changes

Copilot reviewed 4 out of 4 changed files in this pull request and generated no comments.

File Description
mssql-tds/src/io/packet_reader.rs Defines non-consuming scalar probe methods.
mssql-tds/src/datatypes/decoder.rs Uses sync-first scalar reads in decoder paths.
mssql-tds/src/connection/transport/network_transport.rs Delegates probes and retains packet-refill fallback.
mssql-tds/src/connection/transport/buffers.rs Implements scalar probes and unit tests.

💡 Add a code-review agent skill or configure MCP servers for context-aware, tailored reviews. Learn more in the docs.

@saurabh500

Copy link
Copy Markdown
Contributor Author

/azp run

@azure-pipelines

Copy link
Copy Markdown
Azure Pipelines:
Successfully started running 1 pipeline(s).

@github-actions

github-actions Bot commented Aug 15, 2026

Copy link
Copy Markdown

📊 Code Coverage Report

🔥 Diff Coverage

97%

🎯 Overall Coverage

91.5%

📦 Project: mssql-tds + mssql-odbc + mssql-py-core
ℹ️ Note: diff coverage is reported, not enforced.


Diff Coverage

Diff: main...HEAD, staged and unstaged changes

  • mssql-tds/src/connection/tds_client.rs (100%)
  • mssql-tds/src/connection/transport/buffers.rs (100%)
  • mssql-tds/src/connection/transport/network_transport.rs (95.2%): Missing lines 1580-1582,1653,1655
  • mssql-tds/src/connection_provider/tds_connection_provider.rs (100%)
  • mssql-tds/src/core.rs (100%)
  • mssql-tds/src/datatypes/decoder.rs (91.5%): Missing lines 866,1629,1649,1667,1869,2199
  • mssql-tds/src/io/packet_reader.rs (100%)
  • mssql-tds/src/test_client_support.rs (100%)

Summary

  • Total: 369 lines
  • Missing: 11 lines
  • Coverage: 97%

mssql-tds/src/connection/transport/network_transport.rs

  1576     async fn receive_token(
  1577         &mut self,
  1578         context: &ParserContext,
  1579         remaining_request_timeout: Option<Duration>,
! 1580         cancel_handle: Option<&CancelHandle>,
! 1581     ) -> TdsResult<Tokens> {
! 1582         NetworkTransport::receive_token(self, context, remaining_request_timeout, cancel_handle)
  1583             .await
  1584     }
  1585 
  1586     async fn receive_row_into(

  1649             remaining_request_timeout,
  1650             cancel_handle,
  1651             out,
  1652         )
! 1653         .await
  1654     }
! 1655 }
  1656 
  1657 #[async_trait]
  1658 impl crate::connection::transport::tds_transport::TdsTransport for NetworkTransport {
  1659     fn as_writer(&mut self) -> &mut dyn NetworkWriter {

mssql-tds/src/datatypes/decoder.rs

  862         T: TdsPacketReader + Send + Sync,
  863     {
  864         let value: ColumnValues = match byte_len {
  865             1 => ColumnValues::TinyInt(read_sync_first!(reader, try_read_byte, read_byte)),
! 866             2 => ColumnValues::SmallInt(read_sync_first!(reader, try_read_int16, read_int16)),
  867             4 => ColumnValues::Int(read_sync_first!(reader, try_read_int32, read_int32)),
  868             8 => ColumnValues::BigInt(read_sync_first!(reader, try_read_int64, read_int64)),
  869             0 => ColumnValues::Null,
  870             _ => {

  1625                 let length = read_sync_first!(reader, try_read_byte, read_byte);
  1626                 return Self::read_daten(reader, length).await;
  1627             }
  1628             TdsDataType::TimeN => {
! 1629                 let length = read_sync_first!(reader, try_read_byte, read_byte);
  1630                 match length {
  1631                     0 => return Ok(ColumnValues::Null),
  1632                     _ => {
  1633                         return Ok(ColumnValues::Time(

  1645                     }
  1646                 }
  1647             }
  1648             TdsDataType::DateTime2N => {
! 1649                 let length = read_sync_first!(reader, try_read_byte, read_byte);
  1650                 match length {
  1651                     0 => Ok(ColumnValues::Null),
  1652                     _ => {
  1653                         self.read_datetime2(

  1663                     }
  1664                 }
  1665             }?,
  1666             TdsDataType::DateTimeOffsetN => {
! 1667                 let length = read_sync_first!(reader, try_read_byte, read_byte);
  1668                 match length {
  1669                     0 => Ok(ColumnValues::Null),
  1670                     _ => {
  1671                         self.read_datetime_offset(

  1865             // Allocates buffer and reads data
  1866             // Creates SqlString with appropriate encoding type
  1867             // NULL handling works (textptr_len = 0)
  1868             // LCID-based decoding implemented (see sql_string.rs)
! 1869             let text_ptr_len = read_sync_first!(reader, try_read_byte, read_byte) as usize;
  1870 
  1871             let length = if text_ptr_len > 0 {
  1872                 const TIMESTAMP_BYTE_COUNT: usize = 8;
  1873                 reader.skip_bytes(text_ptr_len).await?;

  2195 {
  2196     Ok(match tds_type {
  2197         // BIGVARBINARYTYPE, BIGBINARYTYPE
  2198         TdsDataType::BigVarBinary | TdsDataType::BigBinary => {
! 2199             let _max_length: u16 = read_sync_first!(reader, try_read_uint16, read_uint16);
  2200             if data_length as usize > MAX_ALLOC_SIZE {
  2201                 return Err(crate::error::Error::ProtocolError(format!(
  2202                     "SQL Variant binary data length {data_length} exceeds maximum allowed size of {MAX_ALLOC_SIZE} bytes"
  2203                 )));


🔗 Quick Links

View Azure DevOps Build · Coverage Report

Copilot AI left a comment

Copy link
Copy Markdown
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Pull request overview

Copilot reviewed 4 out of 4 changed files in this pull request and generated no new comments.

Comment thread mssql-tds/src/datatypes/decoder.rs
Co-authored-by: Copilot App <223556219+Copilot@users.noreply.github.com>
Co-authored-by: Copilot App <223556219+Copilot@users.noreply.github.com>

Copilot-Session: 2bf6f59e-c587-4687-93bd-201a9f26681a

Copilot AI left a comment

Copy link
Copy Markdown
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Pull request overview

Copilot reviewed 4 out of 4 changed files in this pull request and generated no new comments.

@saurabh500
Saurabh Singh (saurabh500) marked this pull request as ready for review August 15, 2026 18:28
@saurabh500
Saurabh Singh (saurabh500) requested a review from a team as a code owner August 15, 2026 18:28
Co-authored-by: Copilot App <223556219+Copilot@users.noreply.github.com>

Copilot-Session: 2bf6f59e-c587-4687-93bd-201a9f26681a
Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment

Projects

None yet

Development

Successfully merging this pull request may close these issues.

2 participants